package com.tnovoselec.android.lbs.location;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;


public class DistanceCalculator {

	private static final double RADIUS = 6371000D; // Earth's radius (mean radius = 6 371km)

	private static final int DEGREE_DISTANCE_AT_EQUATOR = 111329; // The length of one degree of latitude (and one degree of longitude at equator) in meters.

	private static final double MINUTES_TO_METERS = 1852D; // The length of one minute of latitude in meters, i.e. one nautical mile in meters.

	private static final double DEGREE_TO_MINUTES = 60D; // The amount of minutes in one degree.

	private DistanceCalculator() {
		super();
	}

	public static double distance(GeoPoint start, GeoPoint end) {
		double lat1 = start.getLatitudeE6() / 1E6;
		double lat2 = end.getLatitudeE6() / 1E6;

		double lon1 = start.getLongitudeE6() / 1E6;
		double lon2 = end.getLongitudeE6() / 1E6;

		double dLat = Math.toRadians(lat2 - lat1);
		double dLon = Math.toRadians(lon2 - lon1);

		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
				Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
				Math.sin(dLon / 2) * Math.sin(dLon / 2);
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

		return DistanceCalculator.RADIUS * c;
	}

	public static int metersToRadius(float meters, MapView mapView, double latitude) {
		return (int) (mapView.getProjection().metersToEquatorPixels(meters) * (1 / Math.cos(Math.toRadians(latitude))));
	}

	/**
	 * This method extrapolates the end point of a movement with a given length from a given starting point using a given course.
	 *
	 * @param startPoint The GeoPoint of the starting point, must not be null.
	 * @param course        The course to be used for extrapolation in degrees, must not be {@link Double#NaN}.
	 * @param distance      The distance to be extrapolated in meters, must not be {@link Double#NaN}.
	 *
	 * @return the extrapolated point.
	*/
	public static GeoPoint extrapolate(final GeoPoint startPoint, final double course, final double distance) {
		return DistanceCalculator.extrapolate(startPoint.getLatitudeE6() / 1E6D, startPoint.getLongitudeE6() / 1E6D, course, distance);
	}

	public static GeoPoint extrapolate(final double startPointLat, final double startPointLon, final double course, final double distance) {
		double crs = Math.toRadians(course);
		double d12 = Math.toRadians(distance / DistanceCalculator.MINUTES_TO_METERS / DistanceCalculator.DEGREE_TO_MINUTES);

		double lat1 = Math.toRadians(startPointLat);
		double lon1 = Math.toRadians(startPointLon);

		double lat = Math.asin(Math.sin(lat1) * Math.cos(d12) + Math.cos(lat1) * Math.sin(d12) * Math.cos(crs));
		double dlon = Math.atan2(Math.sin(crs) * Math.sin(d12) * Math.cos(lat1), Math.cos(d12) - Math.sin(lat1) * Math.sin(lat));
		double lon = (lon1 + dlon + Math.PI) % (2 * Math.PI) - Math.PI;

		return new GeoPoint((int) (Math.toDegrees(lat) * 1E6), (int) (Math.toDegrees(lon) * 1E6));
	}

	/**
	  * Calculates the length of one degree of longitude at the given latitude.
	  *
	  * @param latitude - The latitude to calculate the longitude distance for, must not be {@link Double#NaN}.
	  *
	  * @return The length of one degree of longitude at the given latitude in meters.
	  */
	public static double longitudeDistanceAtLatitude(final double latitude) {
		double longitudeDistanceScaleForCurrentLatitude = Math.cos(Math.toRadians(latitude));

		return DistanceCalculator.DEGREE_DISTANCE_AT_EQUATOR * longitudeDistanceScaleForCurrentLatitude;
	}
}